//=============================================================================
// System  : MatchKit
// File    : HtmlMatchTracer.cs
// Author  : Fabio Bussu  (matchkit@fabiobussu.it)
// Updated : 04/16/2013
// Note    : Copyright 2013, Fabio Bussu, All rights reserved
//
// This code is published under the Microsoft Public License (Ms-PL). A copy
// of the license should be distributed with the code.  It can also be found
// at the project website: http://matchkit.codeplex.com. This notice, the
// author's name, and all copyright notices must remain intact in all
// applications, documentation, and source files.
//
// Version     Date     Who  Comments
// ============================================================================
// 1.0.0.0  04/16/2013   FB  First draft
//=============================================================================
using System;
using System.Net;
using System.Runtime.Serialization;
using System.Text;

namespace MatchKit
{
	/// <summary>
	/// Provides an HTML representation of the match tracer.
	/// </summary>
	/// <threadsafety static="true" instance="false" />
	/// <serialization binary="true" contract="true" />
	/// <conceptualLink target="7e345cd8-5aaa-4616-a3c9-abcc9344dabc" />
	/// <conceptualLink target="0a06b2a9-5f58-409a-ba62-639cfd77bca7" />
	#if DC
	[DataContract]
	#endif
	#if SZ
	[Serializable]
	#endif
	public sealed class HtmlMatchTracer : IMatchTracer
	{
		#if DC
		[DataMember]
		#endif
		private StringBuilder _sb = new StringBuilder(256 * 100);
		#if DC
		[DataMember]
		#endif
		private int _indentCount = 0;
		#if DC
		[DataMember]
		#endif
		private string _indent = string.Empty;

		private int ForwardPadLength { get { return ForwardLength * 2 + 3; } }

		/// <summary>
		/// Gets or sets the length of the preview string for a <see cref="T:MatchKit.TraceEntry"/> location to write in the trace. 
		/// <br />
		/// Default value is <b>10</b>.
		/// </summary>
		/// <serialization member="true" />
		#if DC
		[DataMember]
		#endif
		public int ForwardLength { get; set; }

		/// <summary>
		/// Initializes a new instance of the <see cref="T:MatchKit.HtmlMatchTracer"/> class. 
		/// </summary>
		public HtmlMatchTracer()
		{
			ForwardLength = 10;
			_sb.AppendLine("<span style=\"font-family: Courier; font-size: 8pt; \">");
		}

		private static string Escape(string s)
		{
			return TextMatchTracer.Escape(s);
		}

		private void Indent()
		{
			_indentCount++;
			_indent += "   ";
		}

		private void Unindent()
		{
			if (_indentCount <= 0)
				return;
			_indentCount--;
			_indent = _indent.Substring(0, _indent.Length - 3);
		}

		/// <inheritdoc />
		public override string ToString()
		{
			var html = _sb.ToString();
			html += "</span>";
			return html;
		}

		/// <inheritdoc />
		private string ToHtml(string s)
		{
			var sb = new StringBuilder();
			foreach (char c in s)
			{
				if (c == '\n')
					sb.Append("<br/>");
				else if (char.IsWhiteSpace(c))
					sb.Append("&nbsp;");
				else if (!char.IsLetterOrDigit(c))
					sb.Append(string.Format("&#{0};", (int)c));
				else
					sb.Append(c);
			}
			return sb.ToString();
		}

		/// <inheritdoc />
		public void Write(TraceEntry entry)
		{
			if (entry == null)
				throw new ArgumentNullException("entry", "entry cannot be null");

			string color = "black";
			var preline = "";
			var postline = "";
			bool? indent = null;

			var pattern = entry.Pattern;
			var s = entry.Context.TargetText;
			var match = entry.Match;
			var loc = match != null ? match.Location : entry.Context.Location;

			var forward = "\"" + s.Substring(loc.Index, Math.Min(ForwardLength, s.Length - loc.Index)) + "\"";
			forward = Escape(forward).PadRight(ForwardPadLength, ' ');

			switch (entry.Reason)
			{
				case TraceReason.BeginMatch:
					postline = entry.Message == null ? "Searching for " + ToHtml(pattern.HelpString) : ToHtml(entry.Message);
					indent = true;
					break;

				case TraceReason.MatchFailed:
				case TraceReason.MatchSuccess:
					if (match.Success)
					{
						postline = string.Format("Matched <i>{0}</i> with \"{1}\"", ToHtml(pattern.HelpString), ToHtml(match.MatchedString));
						if (pattern.CaptureName != null)
							postline += " with name (" + ToHtml(pattern.CaptureName) + ")";
						postline += entry.Message != null ? "; " + ToHtml(entry.Message) : string.Empty;
						color = "green";
					}
					else
					{
						postline = "Error at " + loc.Index + "; ";
						if (match.IsOptional)
						{
							postline += "Optional element; ";
							color = "orange";
						}
						else
						{
							color = "red";
						}
						if (entry.Message != null)
							postline += "\"" + ToHtml(entry.Message) + "\"; ";
						else
							postline += "Not Matched; ";
						postline += "\"" + ToHtml(match.ErrorString) + "\"";
					}
					indent = false;
					break;

				case TraceReason.Verbose:
					postline += ToHtml(entry.Message) ?? string.Empty;
					color = "gray";
					break;

				case TraceReason.Warning:
					postline += ToHtml(entry.Message) ?? string.Empty;
					color = "orange";
					break;
			}

			if (indent != null && indent.Value == false)
				Unindent();

			preline = string.Format("{0} {1:0000},{2:000}:{3:00000} {4}<i>({5:00})</i> "
				, ToHtml(forward)					// 0
				, loc.Line, loc.Column, loc.Index	// 1,2,3
				, ToHtml(_indent)					// 4
				, _indentCount						// 5
				);

			_sb.Append("<span style=\"color : " + color + ";\">");
			_sb.Append(preline);
			_sb.Append(postline);
			_sb.AppendLine("</span><br/>");

			if (indent != null && indent.Value == true)
				Indent();
		}
	}
}
